
This tutorial uses data published by NASA:
Hat tip to https://fosstodon.org/@65dBnoise/108251277108722231 for providing the pointers
Known issues:
import numpy as np
import pandas as pd
import geopandas as gpd
import movingpandas as mpd
import shapely as shp
import hvplot.pandas
import matplotlib.pyplot as plt
from geopandas import GeoDataFrame, read_file
from shapely.geometry import Point, LineString, Polygon
from datetime import datetime, timedelta
from holoviews import opts, dim
from os.path import exists
from urllib.request import urlretrieve
import warnings
warnings.filterwarnings('ignore')
plot_defaults = {'linewidth':5, 'capstyle':'round', 'figsize':(9,3), 'legend':True}
opts.defaults(opts.Overlay(active_tools=['wheel_zoom'], frame_width=500, frame_height=400))
hvplot_defaults = {'tiles':None, 'cmap':'Viridis', 'colorbar':True}
mpd.show_versions()
MovingPandas 0.15.rc1 SYSTEM INFO ----------- python : 3.9.15 | packaged by conda-forge | (main, Nov 22 2022, 08:39:05) [MSC v.1929 64 bit (AMD64)] executable : H:\miniconda3\envs\mpd-ex\python.exe machine : Windows-10-10.0.19045-SP0 GEOS, GDAL, PROJ INFO --------------------- GEOS : None GEOS lib : None GDAL : 3.5.0 GDAL data dir: None PROJ : 9.0.0 PROJ data dir: H:\miniconda3\pkgs\proj-9.0.0-h1cfcee9_1\Library\share\proj PYTHON DEPENDENCIES ------------------- geopandas : 0.12.2 pandas : 1.5.3 fiona : 1.8.21 numpy : 1.24.1 shapely : 1.8.2 rtree : 1.0.0 pyproj : 3.3.1 matplotlib : 3.6.3 mapclassify: None geopy : 2.3.0 holoviews : 1.14.9 hvplot : 0.8.2 geoviews : 1.9.6 stonesoup : 0.1b11
"The car-sized Perseverance and its little helicopter buddy Ingenuity landed together inside Mars' Jezero Crater on Feb. 18." https://www.space.com/perseverance-rover-100-mars-days (by Mike Wall published June 02, 2021)
"One sol lasts about 24 hours and 40 minutes, slightly longer than an Earth day." https://www.space.com/perseverance-rover-100-mars-days
def to_timestamp(row):
start_time = datetime(2021,2,18,0,0,0) # sol 0
try:
sol = row['sol'] # rover
except KeyError:
sol = row['Sol'] # heli
td = timedelta(hours=24*sol, minutes=40*sol)
return start_time + td
def get_df_from_url(url):
file = url.split('/')[-1]
if not exists(file):
urlretrieve(url, file)
gdf = read_file(file)
gdf['time'] = gdf.apply(to_timestamp, axis=1)
gdf.set_index('time', inplace=True)
return gdf
m20_waypoints_json = "https://mars.nasa.gov/mmgis-maps/M20/Layers/json/M20_waypoints.json"
heli_waypoints_json = "https://mars.nasa.gov/mmgis-maps/M20/Layers/json/m20_heli_waypoints.json"
m20_df = get_df_from_url(m20_waypoints_json)
heli_df = get_df_from_url(heli_waypoints_json)
print(f'M20 records: {len(m20_df)}')
print(f'Heli records: {len(heli_df)}')
M20 records: 156 Heli records: 29
m20_df.describe()
| site | drive | sol | SCLK_START | SCLK_END | easting | northing | elev_geoid | elev_radii | radius | ... | roll | pitch | yaw | yaw_rad | tilt | dist_m | dist_total | dist_km | dist_mi | ORIG_FID | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 156.000000 | 156.000000 | 156.000000 | 1.560000e+02 | 1.560000e+02 | 1.560000e+02 | 1.560000e+02 | 156.000000 | 156.000000 | 1.560000e+02 | ... | 156.000000 | 156.000000 | 156.000000 | 156.000000 | 156.000000 | 156.000000 | 156.000000 | 156.000000 | 156.000000 | 156.000000 |
| mean | 10.705128 | 1635.076923 | 261.012821 | 6.858288e+08 | 6.858989e+08 | 4.353919e+06 | 1.093211e+06 | -2565.319896 | -4248.434796 | 3.391942e+06 | ... | 0.078588 | 0.877516 | -29.754424 | -0.519312 | 3.247422 | 73.370897 | 3369.154237 | 3.369103 | 2.093141 | 83.500000 |
| std | 7.343391 | 1757.848735 | 141.790576 | 5.665063e+07 | 5.664626e+07 | 9.786968e+02 | 6.396507e+02 | 14.163160 | 13.179175 | 1.317897e+01 | ... | 3.123551 | 3.221801 | 106.107954 | 1.851936 | 3.148950 | 84.848770 | 3668.565027 | 3.668456 | 2.279536 | 45.177428 |
| min | 3.000000 | 0.000000 | 13.000000 | 0.000000e+00 | 0.000000e+00 | 4.351710e+06 | 1.092270e+06 | -2585.869629 | -4266.522461 | 3.391923e+06 | ... | -12.377935 | -7.797700 | -179.475000 | -3.132400 | -0.043088 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 6.000000 |
| 25% | 4.000000 | 410.000000 | 126.750000 | 6.781907e+08 | 6.781920e+08 | 4.353956e+06 | 1.092629e+06 | -2574.016500 | -4255.397319 | 3.391935e+06 | ... | -1.075334 | -0.520792 | -126.751645 | -2.212250 | 1.087888 | 8.878500 | 302.875000 | 0.305000 | 0.187500 | 44.750000 |
| 50% | 9.000000 | 1304.000000 | 330.500000 | 6.962873e+08 | 6.962881e+08 | 4.354411e+06 | 1.093255e+06 | -2569.165000 | -4252.637510 | 3.391937e+06 | ... | -0.120000 | 0.531261 | -58.070700 | -1.013550 | 1.995700 | 36.329000 | 2117.274000 | 2.120000 | 1.315000 | 83.500000 |
| 75% | 17.250000 | 2247.000000 | 395.250000 | 7.020252e+08 | 7.020356e+08 | 4.354547e+06 | 1.093694e+06 | -2558.318000 | -4245.486393 | 3.391945e+06 | ... | 1.179550 | 2.214195 | 45.275105 | 0.790200 | 4.457050 | 111.595500 | 5586.892750 | 5.587500 | 3.475000 | 122.250000 |
| max | 24.000000 | 9436.000000 | 437.000000 | 7.057373e+08 | 7.057424e+08 | 4.355183e+06 | 1.094703e+06 | -2520.911000 | -4204.339000 | 3.391986e+06 | ... | 13.177640 | 19.349400 | 179.893300 | 3.139700 | 19.833200 | 313.832000 | 11445.860000 | 11.450000 | 7.110000 | 161.000000 |
8 rows × 22 columns
m20_df.hvplot(title="M20 & heli waypoints", hover_cols=['sol'], **hvplot_defaults) * heli_df.hvplot()
m20_traj = mpd.Trajectory(m20_df, 'm20')
heli_traj = mpd.Trajectory(heli_df, 'heli')
traj_plot = m20_traj.hvplot(title="M20 & heli trajectories", line_width=3, **hvplot_defaults) * heli_traj.hvplot(line_width=3, **hvplot_defaults)
traj_plot
m20_traj.hvplot(title="Rover speed (only suitable for relative comparison)",
c='speed', line_width=7, **hvplot_defaults)
m20_detector = mpd.TrajectoryStopDetector(m20_traj)
stop_points = m20_detector.get_stop_points(min_duration=timedelta(seconds=60), max_diameter=100)
stop_points['duration_days'] = stop_points['duration_s']/(60*60*24)
stop_points.head()
| geometry | start_time | end_time | traj_id | duration_s | duration_days | |
|---|---|---|---|---|---|---|
| stop_id | ||||||
| m20_2021-03-03 08:40:00 | POINT (77.45095 18.44463) | 2021-03-03 08:40:00 | 2021-03-05 10:00:00 | m20 | 177600.0 | 2.055556 |
| m20_2021-03-10 13:20:00 | POINT (77.45178 18.44516) | 2021-03-10 13:20:00 | 2021-03-22 21:20:00 | m20 | 1065600.0 | 12.333333 |
| m20_2021-03-24 22:40:00 | POINT (77.45102 18.44487) | 2021-03-24 22:40:00 | 2021-04-08 08:00:00 | m20 | 1243200.0 | 14.388889 |
| m20_2021-04-12 10:40:00 | POINT (77.45228 18.44428) | 2021-04-12 10:40:00 | 2021-05-17 09:20:00 | m20 | 3019200.0 | 34.944444 |
| m20_2021-05-31 18:40:00 | POINT (77.45184 18.44351) | 2021-05-31 18:40:00 | 2021-06-02 20:00:00 | m20 | 177600.0 | 2.055556 |
heli_detector = mpd.TrajectoryStopDetector(heli_traj)
heli_stop_points = heli_detector.get_stop_points(min_duration=timedelta(seconds=60), max_diameter=100)
heli_stop_points['duration_days'] = heli_stop_points['duration_s']/(60*60*24)
heli_stop_points.head()
| geometry | start_time | end_time | traj_id | duration_s | duration_days | |
|---|---|---|---|---|---|---|
| stop_id | ||||||
| heli_2021-04-03 04:40:00 | POINT (77.45102 18.44486) | 2021-04-03 04:40:00 | 2021-04-29 22:00:00 | heli | 2308800.0 | 26.722222 |
| heli_2021-08-04 12:40:00 | POINT (77.43916 18.43277) | 2021-08-04 12:40:00 | 2021-10-23 16:40:00 | heli | 6926400.0 | 80.166667 |
| heli_2022-03-23 18:40:00 | POINT (77.44288 18.45068) | 2022-03-23 18:40:00 | 2022-04-03 01:20:00 | heli | 888000.0 | 10.277778 |
stop_point_plot = stop_points.hvplot(title='M20 & heli stops ',
geo=True, size=np.log(dim('duration_days'))*10,
hover_cols=['duration_days'], color='blue', alpha=0.5)
heli_stop_plot = heli_stop_points.hvplot(geo=True, size=np.log(dim('duration_days'))*10,
hover_cols=['duration_days'], color='red', alpha=0.5)
stop_point_plot * heli_stop_plot * traj_plot
Compare to https://mars.nasa.gov/mars2020/mission/where-is-the-rover/
from bokeh.models import TMSTileSource
tile_url = 'http://s3-eu-west-1.amazonaws.com/whereonmars.cartodb.net/celestia_mars-shaded-16k_global/{Z}/{X}/{Y}.png'
def mars_tiles(plot, element):
plot.state.add_tile(TMSTileSource(url=tile_url), level='underlay')
traj_map = m20_traj.hvplot(title="M20 & heli trajectories", tiles=None) * heli_traj.hvplot(**hvplot_defaults)
traj_map.opts(hooks=[mars_tiles])
from geoviews.element import WMTS
MarsImagery = WMTS(
'https://trek.nasa.gov/tiles/Mars/EQ/Mars_MGS_MOLA_ClrShade_merge_global_463m/1.0.0/default/default028mm/{Z}/{Y}/{X}.jpg',
name="Mars")
m20_traj.hvplot(title="M20 & heli trajectories", tiles=MarsImagery) * heli_traj.hvplot(**hvplot_defaults)